Event Scripting
Melodee supports admin-authored JavaScript scripts that can allow/deny specific operations at well-defined hook points (called “events”). Scripts are evaluated in-process using the Jint engine and must return either:
- A boolean:
trueto allow,falseto deny. - An object with
result(boolean) and optionalmessage(string) properties.
Example return values:
// Simple boolean
return true;
// Object with message (message is displayed to users when denied)
return { result: false, message: "Registration is currently disabled for maintenance." };
If a script is missing/disabled, fails to compile, throws, times out, exceeds statement limits, or returns a non-boolean/non-object,
Melodee defaults to allow (true).
Where to manage scripts
In the Melodee web UI:
- Go to Admin → Scripts.
- Select an event (dropdown) and choose Create, or edit an existing script row.
- Use the built-in editor to update script text, enable/disable the config, and run a “Test” using mock JSON.
Scripts are stored in the database Settings table under keys like:
script.<eventName>(example:script.directoryProcessingStart)
Script contract
Melodee calls your script as:
function check(ctx, scriptConfig) {
// Return boolean or object with result/message
return true;
}
You may also provide a single expression; Melodee wraps it into check(...) automatically:
ctx.userNameLength >= 3 && ctx.emailDomain === "example.com"
Return values
Scripts can return:
| Return Type | Example | Behavior |
|---|---|---|
true |
return true; |
Allow the operation |
false |
return false; |
Deny the operation |
Object with result |
return { result: false, message: "Not allowed" }; |
Deny with message displayed to user |
The message property is particularly useful for UI events (login, registration, profile, etc.) where the message
is displayed to the user explaining why the action was denied.
Inputs
ctx: event-specific context object (see event reference below).scriptConfig: metadata about the current evaluation.
scriptConfig fields available in scripts:
| Field | Type | Notes |
|---|---|---|
eventName |
string | The current event name |
settingKey |
string | The settings key, e.g. script.userLoginStart |
timeoutMs |
number | The configured timeout in milliseconds |
maxStatements |
number | The configured statement limit |
onDeny |
string | Host action when result is false (skip, delete, quarantine) |
isOverride |
boolean | Whether an override (library/path) matched |
libraryId |
number|null | The matched override library ID (directory events only) |
pathPrefix |
string|null | The matched override path prefix (directory events only) |
Naming and casing
Melodee exposes ctx and scriptConfig using camelCase keys, even if the underlying .NET models are PascalCase.
For example: ctx.LibraryId becomes ctx.libraryId.
Configuration model (stored as JSON)
Each script.<eventName> setting value is a JSON document. The conceptual schema is:
{
"enabled": true,
"engine": "jint",
"timeoutMs": 50,
"maxStatements": 10000,
"default": {
"enabled": true,
"onDeny": "skip",
"body": "function check(ctx, scriptConfig) { return true; }"
},
"overrides": [
{
"enabled": true,
"libraryId": 1,
"pathPrefix": "Incoming/",
"onDeny": "delete",
"body": "function check(ctx, scriptConfig) { return ctx.mediaFilesCount >= 3; }"
}
]
}
Notes:
enabled: falsedisables scripting for the event and always allows.default.onDenyis used when the default script denies.overridesapply only wherelibraryIdand/orpathPrefixmatches the current directory event.
Override selection rules
For directory events, Melodee chooses at most one override:
- Consider only
overrideswithenabled: true. libraryIdmust match exactly if the override specifies one.pathPrefixmust be a prefix match of the normalized relative path if specified.- The most specific match wins:
- Prefer overrides with a
libraryIdover those without. - Prefer the longest
pathPrefix. - If still tied, the earliest entry in the list wins.
- Prefer overrides with a
Safety and guardrails
Melodee treats scripts as untrusted code (defense in depth):
- Scripts do not receive live .NET objects or direct filesystem/network APIs.
- Execution limits are enforced:
- Time limit (
timeoutMs) - Statement limit (
maxStatements)
- Time limit (
- Failures default to allow and are logged using the settings key and script hash (not the full script body).
Directory deletion and dry-run
Directory deletion is constrained to safe roots. You can also enable dry-run mode:
script.dryRun.enabled = trueprevents deletion/quarantine from actually modifying the filesystem.
Event reference
This section lists the supported events and the ctx fields available to scripts.
directoryProcessingStart
Runs before processing each candidate directory. If it returns false, Melodee applies onDeny (skip, delete, or
quarantine).
Context: DirectoryProcessingContext
| Field | Type | Notes |
|---|---|---|
libraryId |
number | Library ID |
relativePath |
string | Path relative to library root |
directoryName |
string | Directory name only |
totalFilesCount |
number | Total files in directory |
totalSizeMegabytes |
number | Total size (MB) |
mostRecentModified |
string | ISO-8601 timestamp |
mediaFilesCount |
number | Recognized media files |
totalDurationMinutes |
number | Aggregate duration |
trackNumbers |
number[] | Extracted track numbers |
hasTrackNumberGaps |
boolean | Whether track numbering has gaps |
directoryProcessingDelete
Runs when directoryProcessingStart returns false and onDeny is delete. If this script returns true, deletion
proceeds; if false, deletion is skipped (directory is not processed but also not deleted).
Context: DirectoryProcessingContext (same as above).
userRegistrationStart
Runs when a user views the registration page. If the script returns false, registration is disabled and the
message property (if provided) is displayed to the user.
Context: UserRegistrationContext
| Field | Type |
|---|---|
userNameLength |
number |
emailDomain |
string |
clientIp |
string |
userAgent |
string |
now |
string |
userLoginStart
Runs when a user views the login page. If the script returns false, authentication is disabled and the
message property (if provided) is displayed to the user.
Context: UserLoginContext
| Field | Type |
|---|---|
userId |
number|null |
roles |
string[] |
clientIp |
string |
userAgent |
string |
now |
string |
userProfileUpdateStart
Runs when a user views their profile page. If the script returns false, the profile becomes read-only and
the message property (if provided) is displayed to the user.
Context: UserProfileUpdateContext
| Field | Type |
|---|---|
userId |
number |
emailDomain |
string |
profileChangesCount |
number |
clientIp |
string |
userAgent |
string |
now |
string |
playlistCreateStart
Runs when viewing the playlists page. If the script returns false, the “Import Playlist” buttons are disabled
and the message property (if provided) is shown as a tooltip.
Context: PlaylistCreateContext
| Field | Type |
|---|---|
userId |
number |
nameLength |
number |
initialSongCount |
number |
now |
string |
podcastChannelAddStart
Runs when viewing the podcasts page. If the script returns false, the “Add Podcast Channel” button is disabled
and the message property (if provided) is shown as a tooltip.
Context: PodcastChannelAddContext
| Field | Type |
|---|---|
userId |
number |
feedUrl |
string |
isNewSubscription |
boolean |
now |
string |
requestCreateStart
Runs when viewing the requests page. If the script returns false, the “New Request” button is disabled
and the message property (if provided) is shown as a tooltip.
Context: RequestCreateContext
| Field | Type |
|---|---|
userId |
number |
requestType |
string |
isFirstRequestToday |
boolean |
dailyRequestCount |
number |
now |
string |
Examples
These examples are written as check(ctx, scriptConfig) functions, but you can also use expression-only scripts when
they are simple.
Example: require minimum media files to process a directory
Event: directoryProcessingStart
function check(ctx, scriptConfig) {
// Require at least 3 media files; otherwise deny.
return ctx.mediaFilesCount >= 3;
}
Recommended override config example:
{
"enabled": true,
"timeoutMs": 50,
"maxStatements": 10000,
"default": {
"enabled": true,
"onDeny": "skip",
"body": "function check(ctx, scriptConfig) { return ctx.mediaFilesCount >= 3; }"
},
"overrides": [
{
"enabled": true,
"libraryId": 1,
"pathPrefix": "Incoming/",
"onDeny": "delete",
"body": "function check(ctx, scriptConfig) { return ctx.mediaFilesCount >= 3; }"
}
]
}
Example: add an extra safety check before deletion
Event: directoryProcessingDelete
function check(ctx, scriptConfig) {
// Only allow deletion if there are no track number gaps.
return ctx.hasTrackNumberGaps === false;
}
Example: block registration with custom message
Event: userRegistrationStart
function check(ctx, scriptConfig) {
const allowed = ["example.com", "example.org"];
if (!allowed.includes((ctx.emailDomain || "").toLowerCase())) {
return {
result: false,
message: "Registration is only available for example.com and example.org email addresses."
};
}
return true;
}
Example: disable login during maintenance
Event: userLoginStart
function check(ctx, scriptConfig) {
// Maintenance window: deny all logins with a message
return {
result: false,
message: "System is under maintenance. Please try again in 30 minutes."
};
}
Example: restrict playlist creation
Event: playlistCreateStart
function check(ctx, scriptConfig) {
// Require playlist names between 3 and 80 characters.
if (ctx.nameLength < 3 || ctx.nameLength > 80) {
return {
result: false,
message: "Playlist names must be between 3 and 80 characters."
};
}
return true;
}
Example: reject insecure podcast feeds
Event: podcastChannelAddStart
function check(ctx, scriptConfig) {
// Allow only HTTPS feeds.
const url = (ctx.feedUrl || "").toLowerCase();
if (!url.startsWith("https://")) {
return {
result: false,
message: "Only HTTPS podcast feeds are allowed for security reasons."
};
}
return true;
}
Example: limit daily requests
Event: requestCreateStart
function check(ctx, scriptConfig) {
const maxDailyRequests = 5;
if (ctx.dailyRequestCount >= maxDailyRequests) {
return {
result: false,
message: "You have reached the maximum of " + maxDailyRequests + " requests per day."
};
}
return true;
}
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.